跳到主要内容

Go 语言学习-“面向对象”

struct 基本使用

可以使用 type 关键字定义一个别名

type myint int

func main() {
var val myint = 100
fmt.Printf("%T %d", val, val) // main.myint 100
}

定义一个结构体

type Book struct {
title string
auth string
}

func main() {
var book Book
book.title = "平凡的世界"
book.auth = "路遥"
fmt.Printf("%v", book) // %v 可以打印任意一种类型的格式化
// {平凡的世界 路遥}
}

但是注意,直接拿 Book 当函数形参是值传递的

// 这样修改 book 是不会影响原始值的
func changeBook(book Book) {
book.title = "liyuu_"
}

所以要传递指针进去

func changeBook(book *Book) {
book.title = "liyuu_"
}

func main() {
var book Book
book.title = "平凡的世界"
book.auth = "路遥"
changeBook(&book)
fmt.Printf("%v", book) // {liyuu_ 路遥}
}

Go 中的 “方法”

Go 中的类就是结构体绑定方法,但是不是在结构体内部绑定,而是外部绑定

方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。

Go 没有面向对象,而我们知道常见的 Java、C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。

Java 中的:

public class Circle {
private float radius;

private float getArea() {
return 3.14 * radius * radius;
}
}

而到了 Go

type Circle struct {
radius float64
}

func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

下面举个例子:

type Book struct {
title string
auth string
}

func (this Book) Show() {
fmt.Printf("%v ", this)
}

func (this Book) SetTitle(nv string) {
this.title = nv
}

func (this Book) GetAuth() string {
return this.auth
}

func main() {
//var book Book;
//book.auth = "吴承恩"
//book.title = "西游记"
book := Book{auth: "吴承恩", title: "西游记"} // 简写
book.Show()
book.SetTitle("人生")
book.Show()
}

但是注意:打印结果是

{西游记 吴承恩} {西游记 吴承恩} 

因为上面那个 this 只是一个原结构体的拷贝,所以需要改成指针才能更改原始值(这里不用加 & 就能调用是因为,Go 默认加上 & 了)

func (this *Book) SetTitle(nv string) {
this.title = nv
}

// ...
book.SetTitle("人生")
// 打印结果
// {西游记 吴承恩} {人生 吴承恩}

值接受者和指针接收者

对于值接收者,如果调用者也是值对象,那么会将调用者的值拷贝一份,并执行方法,方法的调用不会影响到调用者值。 如果调用者是指针对象,那么会解引用指针对象为值,然后将解引的对象拷贝一份,然后执行方法。

对于指针接收者,如果调用者是值对象,会使用值的引用来调用方法,上例中,book.SetTitle("人生") 实际上是语法糖,实际执行的是 (&book).SetTitle(&book, "人生"),所以传入指针接收者方法的对象地址和调用者地址一样。

同理:

// 改成值方法
func (b Book) SetTitle(nv string) {
b.title = nv
}

// ...

book2 := &Book{"西游记", "吴承恩"}
book2.SetTitle("人生") // 实际上是 (*book2).SetTitle(&book, "人生") `

如果调用者是指针对象,实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针,但是指针指向同一个对象。

编译器里面的 self

在很多其它面向对象的编程语言中,属主参数名总是为隐式声明的 this 或者 self。这样的名称不推荐在 Go 编程中使用。那说回来,那 Go 是如何修改接收者内的值的呢?

如下例子:

type Book struct {
pages int
}

func (b Book) Pages() int {
return b.pages
}

func (b *Book) SetPages(pages int) {
b.pages = pages
}

编译器将自动声明下面的两个函数:

func Book.Pages(b Book) int {
return b.pages // 此函数体和Book类型的Pages方法体一样
}

func (*Book).SetPages(b *Book, pages int) {
b.pages = pages // 此函数体和*Book类型的SetPages方法体一样
}

可以看到,Go 实际上是把这个对象通过入参的方式传递进来

指针方法和值方法

1、如果实现了接收者是值类型的方法,会隐含地也实现接收者是指针类型的方法

2、值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。

但是注意:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

如下例子:

type coder interface {
code()
debug()
}

type Gopher struct {
language string
}

func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()
}

运行一下,结果:

I am coding Go language
I am debuging Go language

但是如果我们把 main 函数的第一条语句换一下(改成值):

func main() {
var c coder = Gopher{"Go"}
c.code()
c.debug()
}

运行一下,报错:

./main.go:24:6: cannot use Programmer literal (type Programmer) as type coder in assignment:
Programmer does not implement coder (debug method has pointer receiver)

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。反之无效

Go 中的 “继承” 与 “重写”

下面再介绍下 “继承” 的写法(Go 的这个其实是组合),go 支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

type Human struct {
name string
sex string
}

func (receiver *Human) Eat() {
fmt.Println("Human Eat()...")
}

func (receiver *Human) Walk() {
fmt.Println("Human Walk()...")
}

/*
创建一个 SupperMan 继承 Human(其实是组合)
*/
type SupperMan struct {
Human // 这个也叫做 “匿名字段”
level int
}

// Eat 重写
func (receiver *SupperMan) Eat() {
fmt.Println("SupperMan Eat()...")
}

func (receiver SupperMan) Fly() {
fmt.Println("SupperMan Fly()...")
}

func main() {
h := Human{name: "zhang", sex: "man"}
h.Eat()
h.Walk()
// 定义子类

s := SupperMan{Human{name: "li", sex: "woman"}, 100}
s.Fly()
s.Eat()
}

打印结果

Human Eat()...
Human Walk()...
SupperMan Fly()...
SupperMan Eat()...

Go 中的 new 关键字

golang 内置函数 new()struct{} 初始化的区别

new() 这是一个用来分配内存的内置函数,它的第一个参数是一个类型,不是一个值,它的返回值是一个指向新分配的 t 类型的零值的指针。

在 golang 的代码定义如下:

func new(t Type) *Type 

直接使用 struct{} 来初始化 strut 时,返回的是一个 struct 类型的值,而不是指针两者是不一样的,两者对比代码如下:

type Student struct {
id int
name string
}

func main(){
var s_1 *Student = new(Student)
s_1.id = 100
s_1.name = "cat"

var s_2 Student = Student{id:1, name:"tom"}
fmt.Println(s_1, s_2)
// 输出结果:&{100 cat} {1 tom}
}

Go 中的接口

Go 中的接口和 TypeScript 的一样,并没有向 Java 那样严格要求必须继承,只需要实现这个接口的方法就行了

  • 在 Java 中:实现接口需要显式地声明接口并实现所有方法;
  • 在 Go 中:实现接口的所有方法就隐式地实现了接口;

定义接口需要使用 interface 关键字,在接口中我们只能定义方法签名,不能包含成员变量

// 例如异常的那个接口
type error interface {
Error() string
}

如果一个类型需要实现 error 接口,那么它只需要实现 Error() string 方法,下面的 RPCError 结构体就是 error 接口的一个实现:

type RPCError struct {
Code int64
Message string
}

func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}

Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查

func main() {
var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
err := AsErr(rpcErr) // typecheck2
println(err)
}

func NewRPCError(code int64, msg string) error {
return &RPCError{ // typecheck3
Code: code,
Message: msg,
}
}

func AsErr(err error) error {
return err
}

显式实现接口的小技巧

Golang 实现接口无需像 Java 那样显示的标注自己是否实现了某个接口,所以有时候可能会出现忘记实现某个接口的情况。

这种时候可以使用下面这种写法

var _ PeerGetter = (*httpGetter)(nil)

这是确保接口被实现常用的方式。即利用强制类型转换,确保 struct httpGetter 实现了接口 PeerGetter。这样 IDE 和编译期间就可以检查,而不是等到使用的时候。

“万能” 接口和断言

Go 中的 interface{} 代表任意类型

type Book struct {
auth string
}

func call(arg interface{}) {
fmt.Printf("%v \n", arg)
}

func main() {
call(Book{auth: "张三"})
call(1000)
call("李四")
}

interface{} 如何区分传入的类型呢?这时就要使用 Go 的断言了

func call(arg interface{}) {
val, ok := arg.(string)
if !ok {
fmt.Printf("arg is not string type %T \n", arg) // %T 打印某个类型的完整说明
} else {
fmt.Printf("arg is string type %s \n", val)
}
}

func main() {
call(Book{auth: "张三"})
call(1000)
call("李四")
}

打印:

arg is not string type main.Book 
arg is not string type int
arg is string type 李四

这个打印的关键字参考 Go 语言中的格式化输出

Go 中的 “构造函数”

构造函数是一种特殊的方法,主要用来在创建对象时初始化对象,即为对象成员变量赋初始值。特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们,即构造函数的重载。

Golang 结构体里面没有构造函数,所以可以像 Java 的 Setter、Getter 那样遵循一个命名规范

示例:

定义一个结构

type ContentMsg struct {
EffectId int `json:"effect_id"`
Text string `json:"text"`
Data interface{} `json:"data"`
}

通过 new 一个对象,或者利用 Golang 本身的 & 方式来生成一个对象并返回一个对象指针:

func NewContentMsg(data, effectId int) *ContentMsg {
instance := new(ContentMsg) // new 传入的是一个类型
instance.Data = data
instance.EffectId = effectId
return instance
}

不过这个本质就是一个静态工厂模式

Reference

值接收者和指针接收者的区别 Receiver Type 34. 图解 Go 语言:静态类型与动态类型 Go 语言设计与实现 深度解密Go语言之关于 interface 的 10 个问题